# 异步上传图片实现
30 秒速记
- 异步上传 = 不刷新页面:前端
FormData+XHR发请求,后端还是busboy落盘,返回图片URL再预览 - 进度用
xhr.upload.onprogress,loaded / total算百分比,fetch目前拿不到上传进度 - 进度到
100%只是请求体发完,服务端解析、写盘、响应还没结束,要区分「上传中 / 处理中 / 成功 / 失败」 - 取消用
xhr.abort(),服务端要监听连接关闭并删掉半截文件 - 量大以后:前端拿短期签名直传对象存储,缩略图、审核这类重活放队列异步做
异步上传就是不刷新页面:前端用 FormData 包好文件,通过 XHR 发给后端,后端用 busboy 写盘后返回图片地址,前端再插一个 <img> 预览。 和表单提交比,它多了两样东西:能拿到上传进度,能留在当前页面处理结果。实际做的时候要分清阶段,进度条 100% 只代表浏览器把数据发完了,服务端可能还在写盘,这时候应该显示「处理中」,等响应回来再判成功或失败。用户量上来后,一般不再让文件经过自己的 Node 服务,而是前端拿一个短期有效的签名直传对象存储。
理解「异步上传图片实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1进度条已经到 100% 了,图片还要好几秒才出来,用户以为卡了,怎么处理?
100% 只是请求体发完了,服务端还在写盘和处理。进度到顶后把文案切成「处理中」,等 onload 拿到响应再显示成功或失败。
追问 2用户点了取消,前端 xhr.abort() 了,服务器上会留下什么?
会留一个半截文件。服务端要监听 ctx.req 的 aborted 或 close,连接断了就销毁写流并删掉文件。
追问 3为什么上传不用 fetch?它不是更现代吗?
fetch 没有上传进度事件,只能拿到下载进度。要显示上传百分比还得用 XHR,或者用 axios,它在浏览器里底层也是 XHR,配置 onUploadProgress 就行。
追问 4图片多了,带宽全耗在 Node 服务上,怎么优化?
改成前端直传对象存储:后端只签发一个几分钟有效、限定 key 前缀、大小和类型的上传凭证,文件不再经过 Node。上传完成后对象存储回调或者前端通知后端落库。
追问 5上传后还要生成缩略图和做内容审核,接口要等这些都做完再返回吗?
一般不等。接口写完原图就返回一个任务 id,缩略图和审核丢进队列,前端轮询或者靠推送拿结果。这样接口响应时间不受图片处理影响,失败也能单独重试。
# 异步上传图片实现
# 快速上手
30 秒速记
demo在daily-code-practice仓库的koa2-demo/upload-async目录- 跑法:
npm install→node index.js→ 打开http://localhost:3000,点按钮选图 - 依赖:
koa、koa-views+ejs、koa-static、busboy;koa-views不自带模板引擎,ejs要单独装 - 上传的图会出现在
static/image/album/,同时页面显示预览
这个 demo 拉下来 npm install、node index.js,打开 3000 端口就能体验异步上传。 它比上一节多了三样东西:用 koa-views 渲染 EJS 页面,用 koa-static 提供前端 JS 和上传后的图片,前端用 XHR 发请求并显示进度。跑起来如果报模板相关的错,多半是没装 ejs,koa-views 只是个壳,底层引擎要自己装。上传完去 static/image/album 看文件,再看页面预览能不能加载出来,两边都对才算跑通。
面试官追问
追问 1启动后访问首页报 Cannot find module 'ejs',怎么回事?
koa-views 通过 consolidate 调用模板引擎,本身不带 ejs。npm install ejs 就好。
追问 2代码里用 koa-convert 包了一层 koa-static,现在还需要吗?
不需要。注释说 koa-static 不支持 Koa 2,那是 koa-static 3.x 的事,4.x 开始就是 async 中间件了,直接 app.use(serve(dir))。
追问 3启动报 EADDRINUSE,怎么处理?
3000 端口被占了,lsof -i :3000 找到占用进程关掉,或者改成 app.listen(process.env.PORT || 3000),用环境变量换端口。
追问 4上传成功了,但预览图是裂的,先看什么?
打开 Network 看图片请求的地址和状态码。pictureUrl 是 //host/image/album/xxx,对应的文件要在 static/image/album/ 下,koa-static 的根目录是 static,对不上就是 404。
# 源码理解
30 秒速记
- 读代码按数据走:
index.ejs按钮 →static/js/index.js选文件、发XHR→index.js路由 →util/upload.js写盘 → 返回pictureUrl - 图片能被访问,是因为写入目录
static/image正好在koa-static的根目录static下面 - 后端不关心前端字段名叫
files还是别的,busboy每个文件都会触发file事件 - 先挂
koa-static、再挂业务中间件,静态文件优先命中,没命中的GET都会渲染首页
这段源码最好顺着一张图片的旅程来读:从按钮点击,到 XHR 发出,到 busboy 写盘,再到 URL 回来变成 <img>。 前端动态创建一个文件 input 并模拟点击,选完图后用 FormData 装起来 POST 到 /api/picture/upload.json,同时用 upload.onprogress 更新进度。后端 koa-static 先拦掉静态资源请求,剩下的 GET 统统渲染首页,POST 到上传接口就调 uploadFile。文件写进 static/image/album,因为它在静态根目录下,所以返回的 //host/image/album/xxx 能直接访问。
理解「源码理解」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
把一张图片在这个 demo 里的完整旅程串起来看,就不用一个文件一个文件地对照了。
1. 浏览器 GET / → koa-static 没找到文件 → 业务中间件 GET 分支 → 渲染 view/index.ejs
2. 浏览器 GET /js/index.js → koa-static 命中 static/js/index.js,直接返回
3. 点按钮 → 动态创建 <input type="file"> 并 click()
4. 选完图片 onchange → new FormData(),append('files', file)
5. XHR POST /api/picture/upload.json,upload.onprogress 不断回调百分比
6. 业务中间件 POST 分支 → uploadFile(ctx, { fileType: 'album', path: static/image })
7. busboy 解析 ctx.req → file 事件 → 写入 static/image/album/<随机名>.<后缀>
8. 返回 { success: true, data: { pictureUrl: '//host/image/album/xxx.png' } }
9. 前端解析 JSON → 插入 <img src=pictureUrl>
10. 浏览器 GET /image/album/xxx.png → koa-static 命中,返回图片
第 2 步和第 10 步说明了一件事:koa-static 的根目录是 static,所以 static/js 和 static/image 都能被直接访问。这对 demo 很方便,但也意味着任何人上传的文件,只要知道地址谁都能访问,而且上传目录和前端代码混在一起,发版时整体覆盖 static 会把用户图片一起删掉。实际项目里会把上传目录和静态代码分开,或者直接用对象存储。
第 7 步和第 8 步之间有一个时间差:代码在读流 end 时就返回了 URL,如果写流还没刷完,第 10 步可能拿到不完整的图片。把成功判定挪到写流
